#include <regex>
#include <system_error>

#include <io.h>
#include <fcntl.h>
#include <ShlObj_core.h>

#include "log.h"
#include "Share.h"
#include "ShareSendProcess.h"

#define COLOR_MASK_COUNT 3
#define RGB_BIT_COUNT 24
#define RGBR_BIT_COUNT 32

// Unicode text data retrieved from windows clipboard with '\0' at the end by default.
// So need to add sizeof(WCHAR) is the size of the data obtained from the clipboard.
#define GET_UNICODE_TEXT_SIZE_MAX ((PLAIN_TEXT_SIZE_MAX + 1) * sizeof(WCHAR))
// The converted utf-8 encoded text also takes the '\0' at the end.
#define GET_UTF8_TEXT_SIZE_MAX (PLAIN_TEXT_SIZE_MAX + 1)

 BOOL ClipboardShareSendTextProcess::Init()
{
    if (m_format & CBSH_FMT_UNICODETEXT) {
        m_lpUnicode = GetClipboardData(CF_UNICODETEXT);
        if (m_lpUnicode == nullptr) {
            LOGE("Get unicodetext clipboardData failed with error: {}", GetLastError());
            return FALSE;
        }

        m_lpUnicodeData = reinterpret_cast<PBYTE>(GlobalLock(m_lpUnicode));
        if (m_lpUnicodeData == nullptr) {
            LOGE("GlobalLock unicodetext data failed with error: {}", GetLastError());
            return FALSE;
        }

        m_header.utf8Len = GlobalSize(m_lpUnicode);
        if (m_header.utf8Len > GET_UNICODE_TEXT_SIZE_MAX) {
            LOGW("Unicode text size: {}, exceeds the maximum size {}", m_header.utf8Len, GET_UNICODE_TEXT_SIZE_MAX);
            return FALSE;
        }

        m_header.utf8Len = WideCharToMultiByte(CP_UTF8, 0, reinterpret_cast<LPCWSTR>(m_lpUnicodeData),
                                               -1, nullptr, 0, nullptr, nullptr);
        if (!m_header.utf8Len) {
            LOGE("WideCharToMultiByte failed with error: {}", GetLastError());
            return FALSE;
        }

        if (m_header.utf8Len > GET_UTF8_TEXT_SIZE_MAX) {
            LOGW("Utf-8 text size: {}, exceeds the maximum size {}", m_header.utf8Len, GET_UTF8_TEXT_SIZE_MAX);
            return FALSE;
        }

        m_utf8Data.resize(m_header.utf8Len);
        if (!WideCharToMultiByte(CP_UTF8, 0, (LPCWSTR)m_lpUnicodeData, -1,
                                 (LPSTR)m_utf8Data.data(), (int)m_header.utf8Len, nullptr, nullptr)) {
            LOGE("WideCharToMultiByte failed with error: {}", GetLastError());
            return FALSE;
        }
    }

    if ((m_format & CBSH_FMT_HTML) && !GetHTMLData()) {
        m_header.htmlLen = 0;
        m_format &= ~CBSH_FMT_HTML;
        LOGE("Failed to get HTML data");
    }
    return TRUE;
}

BOOL ClipboardShareSendTextProcess::GetHTMLData()
{
    m_lpHtml = GetClipboardData(RegisterClipboardFormat(CBSH_WIN_REGFMT_HTML));
    if (m_lpHtml == nullptr) {
        LOGE("Get register HTML clipboard format failed with error: {}", GetLastError());
        return FALSE;
    }

    m_lpHtmlData = (PBYTE)GlobalLock(m_lpHtml);
    if (m_lpHtmlData == nullptr) {
        LOGE("GlobalLock HTML data failed with error: {}", GetLastError());
        return FALSE;
    }

    m_header.htmlLen = GlobalSize(m_lpHtml);
    if (m_header.htmlLen > HTML_SIZE_MAX) {
        LOGW("HTML data size {} exceeds the maximum size {}", m_header.htmlLen, HTML_SIZE_MAX);
        return FALSE;
    }

    if (!CheckHTMLHeader()) {
        LOGE("Failed to check HTML header.");
        return FALSE;
    }

    if (!GetHTMLFiles()) {
        LOGE("Failed to Get HTML Files.");
        return FALSE;
    }

    return TRUE;
}

BOOL ClipboardShareSendTextProcess::CheckHTMLHeader()
{
    if (m_lpHtmlData == nullptr) {
        return FALSE;
    }

    std::regex headerRegex("StartHTML:(\\d+)");
    std::cmatch matchHtmlHeader;
    if (std::regex_search((const char*)m_lpHtmlData, matchHtmlHeader, headerRegex)) {
        try {
            uint32_t offStart = std::stoul(matchHtmlHeader[1]);
            if (offStart >= m_header.htmlLen) {
                LOGE("Offset of <html> exceeds HTML data length.");
                return FALSE;
            }
            m_lpHtmlData += offStart;
            m_header.htmlLen -= offStart;
        } catch (const std::exception& e) {
            LOGE("Failed to get absolute offset of <html> with error: {}", e.what());
            return FALSE;
        }
    } else {
        LOGE("Unknown HTML format, expected StartHTML.");
        return FALSE;
    }

    return TRUE;
}

BOOL ClipboardShareSendTextProcess::GetHTMLFiles()
{
    if (m_lpHtmlData == nullptr) {
        LOGE("HTML data is a null pointer.");
        return FALSE;
    }

    FileShareList fileList;
    std::string filePath;
    std::string fileName;
    const char *searchStart = (const char*)m_lpHtmlData;
    std::regex htmlFilePathReg(HTML_FILE_PATH_REGEX);
    std::cmatch matchesFilePath;
    std::regex fileNameReg(FILE_NAME_REGEX);
    std::cmatch matchFileName;
    int64_t fileSize = 0;

    while (std::regex_search(searchStart, matchesFilePath, htmlFilePathReg)) {
        searchStart = matchesFilePath[1].second;
        filePath = matchesFilePath[1].str();
        if (GetFileSize(filePath.c_str(), fileSize) == FALSE) {
            LOGE("Failed to get file size");
            continue;
        }

        if (fileSize > HTML_FILE_SIZE_MAX) {
            LOGW("HTML file size: {}, exceeds the maximum size {}", fileSize, HTML_FILE_SIZE_MAX);
            continue;
        }

        if (std::regex_search(filePath.c_str(), matchFileName, fileNameReg)) {
            fileName = matchFileName[1];
        } else {
            LOGE("File path parameter is invalid");
            continue;
        }

        fileList.insert({fileName, filePath});
        if (fileList.size() >= HTML_FILES_MAX) {
            break;
        }
    }

    if (!fileList.empty()) {
        std::string share = m_updateWorker(fileList);
        m_sendShareName(share.c_str(), CBSH_HTML_FILES_VIRT_SHARE);
    }

    return TRUE;
}

BOOL ClipboardShareSendTextProcess::GetFileSize(const char *filePath, int64_t &fileSize)
{
    if (filePath == nullptr) {
        LOGE("File path is a null pointer.");
        return FALSE;
    }

    int len = MultiByteToWideChar(CP_UTF8, 0, filePath, -1, nullptr, 0);
    if (len <= 0) {
        LOGE("Convert wide char file path size failed with error: {}", GetLastError());
        return FALSE;
    }

    std::wstring wideFilePath(len, 0);
    if (MultiByteToWideChar(CP_UTF8, 0, filePath, -1, (LPWSTR)wideFilePath.data(), len) <= 0) {
        LOGE("Convert wide char file path failed with error: {}", GetLastError());
        return FALSE;
    }

    struct _stat64 st;
    if (_wstat64(wideFilePath.c_str(), &st) != 0) {
        LOGE("Failed to get file information with error: {}", errno);
        return FALSE;
    }

    fileSize = st.st_size;

    return TRUE;
}

BOOL ClipboardShareSendTextProcess::NextData(PBYTE *dataChunk, DWORD *chunkSize)
{
    if (m_pos >= m_header.utf8Len + m_header.htmlLen) {
        m_pos = 0;
        return FALSE;
    }

    if (m_pos < m_header.utf8Len) {
        *dataChunk = (PBYTE)m_utf8Data.data() + m_pos;
        if (m_pos + MAX_BUF_SIZE >= m_header.utf8Len) {
            *chunkSize = (DWORD)(m_header.utf8Len - m_pos);
            m_pos = m_header.utf8Len;
        } else {
            *chunkSize = MAX_BUF_SIZE;
            m_pos += MAX_BUF_SIZE;
        }
    } else if (m_pos >= m_header.utf8Len && m_pos < m_header.utf8Len + m_header.htmlLen) {
        *dataChunk = m_lpHtmlData + m_pos - m_header.utf8Len;
        if (m_pos + MAX_BUF_SIZE >= m_header.utf8Len + m_header.htmlLen) {
            *chunkSize = (DWORD)(m_header.utf8Len + m_header.htmlLen - m_pos);
            m_pos = m_header.utf8Len + m_header.htmlLen;
        } else {
            *chunkSize = MAX_BUF_SIZE;
            m_pos += MAX_BUF_SIZE;
        }
    }

    return TRUE;
}

DWORD ClipboardShareSendTextProcess::GetFormat()
{
    return m_format & CBSH_UNICODE_AND_HTML;
}

SIZE_T ClipboardShareSendTextProcess::DataLen()
{
    return m_header.utf8Len + m_header.htmlLen;
}

PBYTE ClipboardShareSendTextProcess::ExtraHeader()
{
    return (PBYTE)&m_header;
}

DWORD ClipboardShareSendTextProcess::ExtraHeaderLen()
{
    return (DWORD)sizeof(m_header);
}

ClipboardShareSendTextProcess::~ClipboardShareSendTextProcess()
{
    m_utf8Data.clear();
    if (m_lpUnicode) {
        GlobalUnlock(m_lpUnicode);
        m_lpUnicode = nullptr;
    }
    m_lpUnicodeData = nullptr;

    if (m_lpHtml) {
        GlobalUnlock(m_lpHtml);
        m_lpHtml = nullptr;
    }
    m_lpHtmlData = nullptr;
}

BOOL ClipboardShareSendBMPProcess::Init()
{
    m_hglb = GetClipboardData(CF_DIB);
    if (m_hglb == nullptr) {
        LOGE("Get bitmap clipboardData failed with error: {}", GetLastError());
        return FALSE;
    }

    m_lpmem = reinterpret_cast<PBYTE>(GlobalLock(m_hglb));
    if (m_lpmem == nullptr) {
        LOGE("GlobalLock bitmap data failed with error: {}", GetLastError());
        return FALSE;
    }

    m_size = GlobalSize(m_hglb);
    if (m_size > BITMAP_SIZE_MAX) {
        LOGW("Bitmap size {}, exceeds the maximum size {}", m_size, BITMAP_SIZE_MAX);
        return FALSE;
    }

    PBITMAPINFOHEADER pDibHeader = reinterpret_cast<PBITMAPINFOHEADER>(m_lpmem);
    m_bmpHeader.signature = BMP_FILE_SIGNATURE;
    m_bmpHeader.res1 = m_bmpHeader.res2 = 0;
    m_bmpHeader.size = static_cast<DWORD>(pDibHeader->biSizeImage + pDibHeader->biSize + sizeof(m_bmpHeader));
    m_bmpHeader.dataOffset = static_cast<DWORD>(pDibHeader->biSize + sizeof(m_bmpHeader));
    if (pDibHeader->biBitCount < RGB_BIT_COUNT) {
        m_bmpHeader.size += static_cast<DWORD>(sizeof(RGBQUAD)) * (1 << pDibHeader->biBitCount);
        m_bmpHeader.dataOffset += static_cast<DWORD>(sizeof(RGBQUAD)) * (1 << pDibHeader->biBitCount);
    } else if (pDibHeader->biBitCount == RGBR_BIT_COUNT &&
               pDibHeader->biCompression == BI_BITFIELDS) {
        m_bmpHeader.size += sizeof(RGBQUAD) * COLOR_MASK_COUNT;
        m_bmpHeader.dataOffset += sizeof(RGBQUAD) * COLOR_MASK_COUNT;
    }

    return TRUE;
}

DWORD ClipboardShareSendBMPProcess::GetFormat()
{
    return CBSH_FMT_BITMAP;
}

PBYTE ClipboardShareSendBMPProcess::ExtraHeader()
{
    return reinterpret_cast<PBYTE>(&m_bmpHeader);
}

DWORD ClipboardShareSendBMPProcess::ExtraHeaderLen()
{
    return static_cast<DWORD>(sizeof(m_bmpHeader));
}

BOOL ClipboardShareSendBMPProcess::NextData(PBYTE *dataChunk, DWORD *chunkSize)
{
    if (m_pos >= m_size) {
        m_pos = 0;
        return FALSE;
    }

    *dataChunk = m_lpmem + m_pos;
    if (m_pos + MAX_BUF_SIZE >= m_size) {
        *chunkSize = static_cast<DWORD>(m_size - m_pos);
        m_pos = m_size;
    } else {
        *chunkSize = MAX_BUF_SIZE;
        m_pos += MAX_BUF_SIZE;
    }

    return TRUE;
}

SIZE_T ClipboardShareSendBMPProcess::DataLen()
{
    return m_size;
}

ClipboardShareSendBMPProcess::~ClipboardShareSendBMPProcess()
{
    if (m_hglb) {
        GlobalUnlock(m_hglb);
        m_hglb = nullptr;
    }
    m_lpmem = nullptr;
}

BOOL ClipboardShareSendFilesProcess::Init()
{
    m_hglb = GetClipboardData(CF_HDROP);
    if (m_hglb == nullptr) {
        LOGE("Get files clipboardData failed with error: {}", GetLastError());
        return FALSE;
    }

    m_lpmem = reinterpret_cast<PBYTE>(GlobalLock(m_hglb));
    if (m_lpmem == nullptr) {
        LOGE("GlobalLock files data failed with error: {}", GetLastError());
        return FALSE;
    }

    m_size = GlobalSize(m_hglb);

    LPCWSTR mstr = (LPCWSTR)(m_lpmem + ((DROPFILES*)m_lpmem)->pFiles);
    SIZE_T len = (m_size - ((DROPFILES*)m_lpmem)->pFiles) / sizeof(WCHAR);
    if (mstr[len - 1] != 0) {
        LOGE("Get files clipboard data error.");
        return FALSE;
    }

    if (!AddClipboardVirtFolder(mstr, len)) {
        return FALSE;
    }

    m_lpmem = (LPBYTE)m_data.data();
    m_size = m_data.size();

    return TRUE;
}

BOOL ClipboardShareSendFilesProcess::GetVolumeName(const std::wstring& wstr,
                                                   std::string& fileName,
                                                   std::string& filePath)
{
    // The maximum length of the volume name buffer is MAX_PATH+1
    std::wstring volumeName(MAX_PATH + 1, L'\0');
    if (!GetVolumeInformationW(wstr.c_str(), volumeName.data(), (DWORD)volumeName.size(),
                               NULL, NULL, NULL, NULL, 0)) {
        LOGE("GetVolumeInformationW failed with error: {}", GetLastError());
        return FALSE;
    }

    if (wcslen(volumeName.data()) == 0) {
        if (iswalpha(wstr[0])) {
            volumeName = std::format(L"[{}] Volume", wstr[0]);
        } else {
            LOGE("Failed to get volume name");
            return FALSE;
        }
    }

    WideStrToUTF8(volumeName.data(), fileName, TRUE);
    WideStrToUTF8(wstr.data(), filePath);

    return TRUE;
}

BOOL ClipboardShareSendFilesProcess::GetFileName(const std::wstring& wstr,
                                                 std::string& fileName,
                                                 std::string& filePath)
{
    WideStrToUTF8(wstr.data(), filePath, TRUE);
    size_t lastSlash = filePath.find_last_of(LOG_FOLDER_SEPS);
    if (lastSlash == std::string::npos) {
        LOGE(L"File path parameter is invalid: {}", GET_PRINTABLE_STRING(wstr));
        return FALSE;
    }

    fileName = filePath.substr(lastSlash + 1);
    if (fileName.empty() || fileName[0] == '\0') {
        LOGE(L"Failed to get file name");
        return FALSE;
    }
    return TRUE;
}

BOOL ClipboardShareSendFilesProcess::GetFileSystem(const std::wstring& wstr, BOOL& isHwFsFileSystem)
{
    /* Ensure the file path is at least 2 characters long.
     * This check prevents invalid paths that are too short to be valid,
     * such as a single character or an empty string.
     */
    if (wstr.size() < 2) {
        LOGE("File path parameter is invalid");
        return FALSE;
    }

    std::wstring rootDir;
    if (iswalpha(wstr[0]) && wstr[1] == L':') {
        // If the path starts with a drive letter, e.g., "C:\"
        rootDir = wstr[0] + std::wstring(L":\\");
    } else if (wstr[0] == L'\\' && wstr[1] == L'\\') {
        // If the path starts with a UNC path, e.g., "\\Server\Share"
        std::wregex UNCRootDirReg(UNC_ROOT_DIR_W_REGEX);
        std::wsmatch matchUNCRootDir;
        if (std::regex_search(wstr, matchUNCRootDir, UNCRootDirReg)) {
            rootDir = matchUNCRootDir[0];
            rootDir += L'\\';
        } else {
            LOGE("Failed to search UNC root directory");
            return FALSE;
        }
    } else {
        LOGE(L"File path parameter is invalid: {}", GET_PRINTABLE_STRING(wstr));
        return FALSE;
    }

    // The maximum length of the file system name buffer is MAX_PATH+1
    std::wstring fileSystemName(MAX_PATH + 1, L'\0');
    if (!GetVolumeInformationW(rootDir.data(), NULL, NULL, NULL, NULL, NULL,
                               fileSystemName.data(), (DWORD)fileSystemName.size())) {
        LOGE("GetVolumeInformationW failed with error: {}", GetLastError());
        return FALSE;
    }

    if (wcscmp(fileSystemName.data(), HWFS_FILE_SYSTEM_NAME) == 0) {
        isHwFsFileSystem = TRUE;
    }

    return TRUE;
}

BOOL ClipboardShareSendFilesProcess::IsPathExists(const std::filesystem::path& path)
{
    std::error_code errorCode;
    BOOL result = std::filesystem::exists(path, errorCode);
    if (errorCode) {
        LOGW("Failed to check path exists with error: {}", errorCode.value());
        return FALSE;
    }

    if (!result) {
        LOGW(L"Can't find the specified path: {}", GET_PRINTABLE_STRING(path));
        return FALSE;
    }

    return TRUE;
}

BOOL ClipboardShareSendFilesProcess::AddClipboardVirtFolder(LPCWSTR mstr, SIZE_T len)
{
    FileShareList fileList;
    BOOL checkOnce = TRUE;

    for (LPCWSTR pos = mstr; pos < mstr + len && *pos; pos += (wcslen(pos) + 1)) {
        if (_wcsnicmp(pos, HWFS_CLIPBOARD_SHARES_PREFIX, wcslen(HWFS_CLIPBOARD_SHARES_PREFIX)) == 0) {
            return FALSE;
        }

        std::wstring wstr = pos;

        /* Assume that the user has copied multiple files or
         * folders on the Windows side into the same folder,
         * we only need to check once.
         */
        if (checkOnce) {
            checkOnce = FALSE;
            BOOL isHWFSFileSystem = FALSE;

            if (IsPathExists(wstr) && !GetFileSystem(wstr, isHWFSFileSystem)) {
                return FALSE;
            }

            if (isHWFSFileSystem) {
                LOGW("Files in shared folders are not supported");
                return FALSE;
            }
        }

        std::string fileName;
        std::string filePath;
        size_t wstrSize = wstr.size();
        /* If the path length is greater than 3, it handles typical
         * file paths longer than "C:\", such as "C:\folder\file.txt".
         */
        if (wstrSize > 3) {
            if (!GetFileName(wstr, fileName, filePath)) {
                continue;
            }
        /* If the path length is exactly 3 or 2, it could be a root directory like "C:\" or "C:". */
        } else if (wstrSize == 3 || wstrSize == 2) {
            if (!GetVolumeName(wstr, fileName, filePath)) {
                continue;
            }
        } else {
            LOGE(L"File path parameter is invalid: {}", GET_PRINTABLE_STRING(wstr));
            continue;
        }

        fileList.insert({ fileName.data(), filePath.data() });
        m_data.insert(m_data.end(), fileName.begin(), fileName.end());
    }

    if (fileList.empty()) {
        LOGE("Failed to get file list.");
        return FALSE;
    }

    std::string share = m_updateWorker(fileList);
    m_header.resize(share.length() + 1);
    INT ret = memcpy_s(m_header.data(), m_header.size(), share.c_str(), share.length() + 1);
    if (ret != 0) {
        LOGE("Execution of memcpy_s fails with error: {}", ret);
        return FALSE;
    }
    return TRUE;
}

DWORD ClipboardShareSendFilesProcess::GetFormat()
{
    return CBSH_FMT_URI_LIST;
}

PBYTE ClipboardShareSendFilesProcess::ExtraHeader()
{
    return (PBYTE)m_header.data();
}

DWORD ClipboardShareSendFilesProcess::ExtraHeaderLen()
{
    return (DWORD)m_header.size();
}

BOOL ClipboardShareSendFilesProcess::NextData(PBYTE *dataChunk, DWORD *chunkSize)
{
    if (m_pos >= m_size) {
        m_pos = 0;
        return FALSE;
    }

    *dataChunk = m_lpmem + m_pos;
    if (m_pos + MAX_BUF_SIZE >= m_size) {
        *chunkSize = static_cast<DWORD>(m_size - m_pos);
        m_pos = m_size;
    } else {
        *chunkSize = MAX_BUF_SIZE;
        m_pos += MAX_BUF_SIZE;
    }

    return TRUE;
}

SIZE_T ClipboardShareSendFilesProcess::DataLen()
{
    return m_size;
}

ClipboardShareSendFilesProcess::~ClipboardShareSendFilesProcess()
{
    if (m_hglb) {
        GlobalUnlock(m_hglb);
        m_hglb = nullptr;
    }
    m_lpmem = nullptr;
}